package NEHE.JOGL.Port;

import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.media.opengl.GL;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLCanvas;
import javax.media.opengl.GLEventListener;
import javax.media.opengl.GLException;
import javax.media.opengl.glu.GLU;
import javax.media.opengl.glu.GLUquadric;
import javax.swing.*;
import com.sun.opengl.util.FPSAnimator;
import com.sun.opengl.util.texture.Texture;
import com.sun.opengl.util.texture.TextureCoords;
import com.sun.opengl.util.texture.TextureIO;
import static javax.media.opengl.GL.*;
import static java.awt.event.KeyEvent.*;

/**
 * NeHe Lesson 18: Quadrics
 */
public class NeheJOGL18Quadrics extends JPanel implements GLEventListener, KeyListener {
   private static final int REFRESH_FPS = 60;    // Display refresh frames per second
   private GLU glu;             // For the GL Utility
   final FPSAnimator animator;  // Used to drive display() 
   GLCanvas canvas;
   
   private int part1; // Start Of Disc  ( NEW )
   private int part2; // End Of Disc    ( NEW )
   private int p1 = 0; // Increase 1     ( NEW )
   private int p2 = 1; // Increase 2     ( NEW )

   private GLUquadric quadric;
   int numObjects = 6;
   int object = 0; // Which Object To Draw ( NEW )

   private static float rotateAngleX = 0; // rotational angle for x-axis in degree
   private static float rotateAngleY = 0; // rotational angle for y-axis in degree
   private static float z = -5.0f; // z-location
   private static float rotateSpeedX = 0.0f; // rotational speed for x-axis
   private static float rotateSpeedY = 0.0f; // rotational speed for y-axis

   private static float zIncrement = 0.5f; // for zoom in/out
   private static float rotateSpeedXIncrement = 0.5f; // adjusting x rotational speed
   private static float rotateSpeedYIncrement = 0.5f; // adjusting y rotational speed

   // Textures with three different filters - Nearest, Linear & MIPMAP
   private Texture[] textures = new Texture[3];
   private static int currTextureFilter = 0; // currently used filter
   private String textureFileName = "images/wall.bmp";

   // Texture image flips vertically. Shall use TextureCoords class to retrieve the
   // top, bottom, left and right coordinates.
   private float textureTop;
   private float textureBottom;
   private float textureLeft;
   private float textureRight;

   // Lighting
   private static boolean lightOn = false;

   // Constructor
   public NeheJOGL18Quadrics() {
      canvas = new GLCanvas();
      this.setLayout(new BorderLayout());
      this.add(canvas, BorderLayout.CENTER);
      canvas.addGLEventListener(this);
      canvas.addKeyListener(this);
      canvas.setFocusable(true);  // To receive key event
      canvas.requestFocus();
   
      // Run the animation loop using the fixed-rate Frame-per-second animator,
      // which calls back display() at this fixed-rate (FPS).
      animator = new FPSAnimator(canvas, REFRESH_FPS, true);
   }

   // Main program
   public static void main(String[] args) {
      final int WINDOW_WIDTH = 320;
      final int WINDOW_HEIGHT = 240;
      final String WINDOW_TITLE = "Nehe #18: Quadrics";

      JFrame frame = new JFrame();
      final NeheJOGL18Quadrics joglMain = new NeheJOGL18Quadrics();
      frame.setContentPane(joglMain);
      frame.addWindowListener(new WindowAdapter() {
         @Override 
         public void windowClosing(WindowEvent e) {
            // Use a dedicate thread to run the stop() to ensure that the
            // animator stops before program exits.
            new Thread() {
               @Override 
               public void run() {
                  joglMain.animator.stop(); // stop the animator loop
                  System.exit(0);
               }
            }.start();
         }
      });
      frame.setSize(WINDOW_WIDTH, WINDOW_HEIGHT);
      frame.setTitle(WINDOW_TITLE);
      frame.setVisible(true);
      joglMain.animator.start(); // start the animation loop
   }

   // ------ Implement methods declared in GLEventListener ------

   /**
    * Called by the drawable immediately after the OpenGL context is initialized for
    * the first time. Can be used to perform one-time OpenGL initialization such as
    * setup of lights and display lists. Run only once.
    */
   @Override
   public void init(GLAutoDrawable drawable) {
      // Get the OpenGL graphics context
      GL gl = drawable.getGL();
      // GL Utilities
      glu = new GLU();

      // Load the texture image
      try {
         // Use URL so that can read from JAR and disk file.
         BufferedImage image = ImageIO.read(this.getClass().getResource(
               textureFileName));

         // Create a OpenGL Texture object from (URL, mipmap, file suffix)
         textures[0] = TextureIO.newTexture(image, false);
         // Nearest filter is least compute-intensive
         // Use nearer filter if image is larger than the original texture
         gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
         // Use nearer filter if image is smaller than the original texture
         gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
         //         gl.glTexImage2D(GL.GL_TEXTURE_2D, 0, 3, textures[0].getWidth(), textures[0]
         //               .getHeight(), 0, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, textures[0].getPixels());

         textures[1] = TextureIO.newTexture(image, false);
         // Linear filter is more compute-intensive
         // Use linear filter if image is larger than the original texture
         gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
         // Use linear filter if image is smaller than the original texture
         gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

         textures[2] = TextureIO.newTexture(image, true); // mipmap is true
         // Use mipmap filter is the image is smaller than the texture
         gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
         gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
               GL_LINEAR_MIPMAP_NEAREST);

         // Get the top and bottom coordinates of the textures. Image flips vertically.
         TextureCoords textureCoords;
         textureCoords = textures[0].getImageTexCoords();
         textureTop = textureCoords.top();
         textureBottom = textureCoords.bottom();
         textureLeft = textureCoords.left();
         textureRight = textureCoords.right();
      } catch (GLException e) {
         e.printStackTrace();
      } catch (IOException e) {
         e.printStackTrace();
      }

      // Enable smooth shading, which blends colors nicely across a polygon, and
      // smoothes out lighting.
      gl.glShadeModel(GL_SMOOTH);
      // Set background color (in RGBA). Alpha of 0 for total transparency
      gl.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
      // Setup the depth buffer & enable the depth testing
      gl.glClearDepth(1.0f);
      gl.glEnable(GL_DEPTH_TEST); // enables depth testing
      gl.glDepthFunc(GL_LEQUAL); // the type of depth test to do
      // We want the best perspective correction to be done
      gl.glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);

      // Set up the lighting for light named GL_LIGHT1

      // Ambient light does not come from a particular direction. Need some ambient
      // light to light up the scene. Ambient's value in RGBA
      float[] lightAmbientValue = { 0.5f, 0.5f, 0.5f, 1.0f };
      // Diffuse light comes from a particular location. Diffuse's value in RGBA
      float[] lightDiffuseValue = { 1.0f, 1.0f, 1.0f, 1.0f };
      // Diffuse light location xyz (in front of the screen).
      float[] lightDiffusePosition = { 0.0f, 0.0f, 2.0f, 1.0f };

      gl.glLightfv(GL_LIGHT1, GL_AMBIENT, lightAmbientValue, 0);
      gl.glLightfv(GL_LIGHT1, GL_DIFFUSE, lightDiffuseValue, 0);
      gl.glLightfv(GL_LIGHT1, GL_POSITION, lightDiffusePosition, 0);
      gl.glEnable(GL_LIGHT1);

      // Set up the Quadrics 
      quadric = glu.gluNewQuadric(); // Create A Pointer To The Quadric Object ( NEW )
      glu.gluQuadricNormals(quadric, GLU.GLU_SMOOTH); // Create Smooth Normals ( NEW )
      glu.gluQuadricTexture(quadric, true); // Create Texture Coords ( NEW )
   }

   /**
    * Called by the drawable to initiate OpenGL rendering.
    */
   @Override
   public void display(GLAutoDrawable drawable) {
      // Get the OpenGL graphics context
      GL gl = drawable.getGL();
      // Clear the screen and the depth buffer
      gl.glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
      // Reset the view (x, y, z axes back to normal)
      gl.glLoadIdentity();

      // Check whether light shall be turn on (toggle via the 'L' key)
      // This LIGHTING is different form LIGHT1 (??)
      if (lightOn) {
         gl.glEnable(GL_LIGHTING);
      } else {
         gl.glDisable(GL_LIGHTING);
      }

      // Enables this texture's target (e.g., GL_TEXTURE_2D) in the current GL
      // context's state.
      textures[currTextureFilter].enable();
      // Bind the texture with the currently chosen filter to the current OpenGL
      // graphics context.
      textures[currTextureFilter].bind();

      gl.glTranslatef(0.0f, 0.0f, z); // translate into the screen
      gl.glRotatef(rotateAngleX, 1.0f, 0.0f, 0.0f); // rotate about the x-axis
      gl.glRotatef(rotateAngleY, 0.0f, 1.0f, 0.0f); // rotate about the y-axis

      switch (object) { // Check object To Find Out What To Draw
         case 0: // Drawing Object 1
            drawCube(gl); // Draw Our Cube
            break;
         case 1: // Drawing Object 2
            // The first parameter (1.0f) is the radius of the cylinder at base (bottom). The second parameter (1.0f) is the radius of the cylinder at the top. The third parameter ( 3.0f) is the height of the cylinder (how long it is). The fouth parameter (32) is how many subdivisions there are "around" the Z axis, and finally, the fifth parameter (32) is the amount of subdivisions "along" the Z axis. The more subdivisions there are the more detailed the object is.
            gl.glTranslatef(0.0f, 0.0f, -1.5f); // Center The Cylinder
            glu.gluCylinder(quadric, 1.0f, 1.0f, 3.0f, 32, 32); // Draw Our Cylinder
            break;
         case 2: // Drawing Object 3
            // CD shaped disc. The first parameter (0.5f) is the inner radius of the disk. This value can be zero, meaning there will be no hole in the middle. The larger the inner radius is, the bigger the hole in the middle of the disc will be. The second parameter (1.5f) is the outer radius. This value should be larger than the inner radius. If you make this value a little bit larger than the inner radius you will end up with a thing ring. If you make this value alot larger than the inner radius you will end up with a thick ring. The third parameter (32) is the number of slices that make up the disc. Think of slices like the slices in a pizza. The more slices you have, the smoother the outer edge of the disc will be. Finally the fourth parameter (32) is the number of rings that make up the disc.
            glu.gluDisk(quadric, 0.5f, 1.5f, 32, 32); // Draw A Disc (CD Shape)
            break;
         case 3: // Drawing Object 4
            //  The Sphere! This one is quite simple. The first parameter is the radius of the sphere. In case you're not familiar with radius/diameter, etc, the radius is the distance from the center of the object to the outside of the object. In this case our radius is 1.3f. Next we have our subdivision "around" the Z axis (32), and our subdivision "along" the Z axis (32).
            glu.gluSphere(quadric, 1.3f, 32, 32); // Draw A Sphere
            break;
         case 4: // Drawing Object 5
            // To make a cone it makes sense that all we'd have to do is make the radius at one end Zero. This will create a point at one end.
            gl.glTranslatef(0.0f, 0.0f, -1.5f); // Center The Cone
            glu.gluCylinder(quadric, 1.0f, 0.0f, 3.0f, 32, 32); // A Cone With A Bottom Radius Of .5 And A Height Of 2
            break;
         case 5: // Drawing Object 6
            // gluPartialDisc. The object we create using this command will look exactly like the disc we created above, but with the command gluPartialDisk there are two new parameters. The fifth parameter (part1) is the start angle we want to start drawing the disc at. The sixth parameter is the sweep angle. The sweep angle is the distance we travel from the current angle. We'll increase the sweep angle, which causes the disc to be slowly drawn to the screen in a clockwise direction. Once our sweep hits 360 degrees we start to increase the start angle.
            part1 += p1; // Increase Start Angle
            part2 += p2; // Increase Sweep Angle

            if (part1 > 359) // 360 Degrees
            {
               p1 = 0; // Stop Increasing Start Angle
               part1 = 0; // Set Start Angle To Zero
               p2 = 1; // Start Increasing Sweep Angle
               part2 = 0; // Start Sweep Angle At Zero
            }
            if (part2 > 359) // 360 Degrees
            {
               p1 = 1; // Start Increasing Start Angle
               p2 = 0; // Stop Increasing Sweep Angle
            }
            glu.gluPartialDisk(quadric, 0.5f, 1.5f, 32, 32, part1, part2 - part1); // A Disk Like The One Before
            break; // Done
      }

      // Disables this texture's target (e.g., GL_TEXTURE_2D) in the current GL
      // context's state.
      textures[currTextureFilter].disable();

      // Update the rotational position after each refresh, based on rotational
      // speed.
      rotateAngleX += rotateSpeedX;
      rotateAngleY += rotateSpeedY;
   }

   public void drawCube(GL gl) {
      // ------ Render a Cube with texture ------

      gl.glBegin(GL_QUADS); // of the color cube
      // Define groups of 4 vertices in CCW order

      // Front Face
      gl.glNormal3f(0.0f, 0.0f, 1.0f);
      gl.glTexCoord2f(textureLeft, textureBottom);
      gl.glVertex3f(-1.0f, -1.0f, 1.0f); // bottom-left of the texture and quad
      gl.glTexCoord2f(textureRight, textureBottom);
      gl.glVertex3f(1.0f, -1.0f, 1.0f); // bottom-right of the texture and quad
      gl.glTexCoord2f(textureRight, textureTop);
      gl.glVertex3f(1.0f, 1.0f, 1.0f); // top-right of the texture and quad
      gl.glTexCoord2f(textureLeft, textureTop);
      gl.glVertex3f(-1.0f, 1.0f, 1.0f); // top-left of the texture and quad
      // Back Face
      gl.glNormal3f(0.0f, 0.0f, -1.0f);
      gl.glTexCoord2f(textureRight, textureBottom);
      gl.glVertex3f(-1.0f, -1.0f, -1.0f);
      gl.glTexCoord2f(textureRight, textureTop);
      gl.glVertex3f(-1.0f, 1.0f, -1.0f);
      gl.glTexCoord2f(textureLeft, textureTop);
      gl.glVertex3f(1.0f, 1.0f, -1.0f);
      gl.glTexCoord2f(textureLeft, textureBottom);
      gl.glVertex3f(1.0f, -1.0f, -1.0f);
      // Top Face
      gl.glNormal3f(0.0f, 1.0f, 0.0f);
      gl.glTexCoord2f(textureLeft, textureTop);
      gl.glVertex3f(-1.0f, 1.0f, -1.0f);
      gl.glTexCoord2f(textureLeft, textureBottom);
      gl.glVertex3f(-1.0f, 1.0f, 1.0f);
      gl.glTexCoord2f(textureRight, textureBottom);
      gl.glVertex3f(1.0f, 1.0f, 1.0f);
      gl.glTexCoord2f(textureRight, textureTop);
      gl.glVertex3f(1.0f, 1.0f, -1.0f);
      // Bottom Face
      gl.glNormal3f(0.0f, -1.0f, 0.0f);
      gl.glTexCoord2f(textureRight, textureTop);
      gl.glVertex3f(-1.0f, -1.0f, -1.0f);
      gl.glTexCoord2f(textureLeft, textureTop);
      gl.glVertex3f(1.0f, -1.0f, -1.0f);
      gl.glTexCoord2f(textureLeft, textureBottom);
      gl.glVertex3f(1.0f, -1.0f, 1.0f);
      gl.glTexCoord2f(textureRight, textureBottom);
      gl.glVertex3f(-1.0f, -1.0f, 1.0f);
      // Right face
      gl.glNormal3f(1.0f, 0.0f, 0.0f);
      gl.glTexCoord2f(textureRight, textureBottom);
      gl.glVertex3f(1.0f, -1.0f, -1.0f);
      gl.glTexCoord2f(textureRight, textureTop);
      gl.glVertex3f(1.0f, 1.0f, -1.0f);
      gl.glTexCoord2f(textureLeft, textureTop);
      gl.glVertex3f(1.0f, 1.0f, 1.0f);
      gl.glTexCoord2f(textureLeft, textureBottom);
      gl.glVertex3f(1.0f, -1.0f, 1.0f);
      // Left Face
      gl.glNormal3f(-1.0f, 0.0f, 0.0f);
      gl.glTexCoord2f(textureLeft, textureBottom);
      gl.glVertex3f(-1.0f, -1.0f, -1.0f);
      gl.glTexCoord2f(textureRight, textureBottom);
      gl.glVertex3f(-1.0f, -1.0f, 1.0f);
      gl.glTexCoord2f(textureRight, textureTop);
      gl.glVertex3f(-1.0f, 1.0f, 1.0f);
      gl.glTexCoord2f(textureLeft, textureTop);
      gl.glVertex3f(-1.0f, 1.0f, -1.0f);

      gl.glEnd();
   }

   /**
    * Called by the drawable during the first repaint after the component has been
    * resized. Run at least once when the window is first set to visible.
    */
   @Override
   public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
      // Get the OpenGL graphics context
      GL gl = drawable.getGL();

      if (height == 0) {
         height = 1; // prevent divide by zero
      }
      float aspect = (float)width / height;

      // Set the viewport (display area) to cover the entire window
      gl.glViewport(0, 0, width, height);

      // Enable perspective view (where far objects are smaller)
      gl.glMatrixMode(GL_PROJECTION);
      gl.glLoadIdentity(); // reset
      // View angle of 45 degrees, based on this canvas's aspect ratio. The 0.1f,
      // 100.0f is the starting and ending points for how deep we can draw into
      // the screen.
      glu.gluPerspective(45.0f, aspect, 0.1f, 100.0f); // fovy, aspect, zNear, zFar

      // Enable the model view - any new transformations will affect the model-view
      // matrix
      gl.glMatrixMode(GL_MODELVIEW);
      gl.glLoadIdentity(); // reset
   }

   /**
    * Called when the display mode (eg. resolution) has been changed.
    */
   @Override
   public void displayChanged(GLAutoDrawable drawable, boolean modeChanged,
         boolean deviceChanged) {}

   // ----- Implement methods declared in KeyListener -----

   @Override
   public void keyPressed(KeyEvent e) {
      switch (e.getKeyCode()) {
         case VK_L: // toggle light on/off
            lightOn = !lightOn;
            break;
         case VK_F: // switch to the next filter (NEAREST, LINEAR, MIPMAP)
            currTextureFilter = (currTextureFilter + 1) % textures.length;
            break;
         case VK_PAGE_UP: // zoom-out
            z -= zIncrement;
            break;
         case VK_PAGE_DOWN: // zoom-in
            z += zIncrement;
            break;
         case VK_UP: // decrease rotational speed in x
            rotateSpeedX -= rotateSpeedXIncrement;
            break;
         case VK_DOWN: // increase rotational speed in x
            rotateSpeedX += rotateSpeedXIncrement;
            break;
         case VK_LEFT: // decrease rotational speed in y
            rotateSpeedY -= rotateSpeedYIncrement;
            break;
         case VK_RIGHT: // increase rotational speed in y
            rotateSpeedY += rotateSpeedYIncrement;
            break;
         case VK_SPACE: // switch into the next object
            object = (object + 1) % numObjects;
            break;
      }
   }

   @Override
   public void keyReleased(KeyEvent e) {}

   @Override
   public void keyTyped(KeyEvent e) {}
}
